feat(acp): enforce deterministic agent git commit identity - #6177
feat(acp): enforce deterministic agent git commit identity#6177wpfleger96 wants to merge 20 commits into
Conversation
Agent sessions could commit under the human operator's ambient git identity: only buzz-dev-mcp's shim applied the nostr author/signing GIT_CONFIG_* env, and only to its own shell-tool children. The native shells of claude-code, codex, and goose never saw it, so a bare `git commit` there resolved to whatever the repo/global config carried — erasing the AI-attribution signal (e.g. block/buzz #3140). Make the identity machine-managed across every harness: - New `buzz-git-identity` crate holds the pure author/email/signing/ keyfile logic as the single source of truth, consumed by both the shim and the harness so an agent commits under a byte-identical identity regardless of which surface applied it. - A `git` enforcement wrapper (installed on PATH ahead of the real binary) scrubs GIT_AUTHOR_*/GIT_COMMITTER_* from the child env, rejects `-c user.*`, `--config-env=user.*`, `--author`, and `--reset-author`, and on push refuses any outgoing commit not authored by the agent identity, then execs real git. - The harness lifts the identity + NIP-GS signing config onto the agent-runtime child and installs the wrapper plus the nostr signer/credential helpers via buzz-acp's own multicall, so native shells of all runtimes inherit both. Composed over the desktop's per-URL credential helper; skipped when no nostr key is present. - Prompt guidance (base_prompt.md, nest_agents.md) updated: identity is machine-managed; credit the operator via Co-authored-by/ Signed-off-by trailers, never user.name/email/-c/--author. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Round-1 review fixes on the deterministic agent-commit-identity work: - L3 push gate resolved the effective command through git aliases (config-defined and inline -c alias.x=push) so a push disguised as a custom alias can no longer skip outgoing-author verification. - Verification subprocesses now carry repository context (-C, --git-dir, --work-tree, --namespace); an outgoing tip that resolves to a real ref but whose range cannot be computed fails closed instead of being skipped as nothing-to-check. - AcpClient::shutdown deletes the git-identity keyfile tempdir explicitly via TempDir::close before the process-group kill. Relying on Drop right before std::process::exit leaked the 0600 nostr keyfile ~80% of runs; all client-owning error/timeout exits funnel through shutdown_and_exit, which takes the client by value. Checkpoint commit: the review round continues on this branch with the identity-authority and push-boundary rework. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The enforcement wrapper trusted the same caller-mutable GIT_CONFIG_* environment it was meant to constrain, so `env -u GIT_CONFIG_COUNT git commit` fell back to repo-local human identity and the push gate derived its expected identity from that same mutable config and failed open. - Authority: harness/shim write a 0600 identity manifest beside the keyfile; the wrapper locates its own install dir by PATH canonicalization, re-applies identity+signing GIT_CONFIG_* at the highest index before exec, and reads L3's expected author from the manifest. Manifest present = enforce; absent = passthrough. - L1b eligibility sources the key from BUZZ_PRIVATE_KEY then NOSTR_PRIVATE_KEY, decoupled from credential-helper discovery, and fails the managed session closed when identity cannot be installed. - Push gate uses git's own resolved plan (push --dry-run --porcelain --no-verify) instead of predicting argv, covering aliases, -C, --all/--mirror/--tags, config refspecs; unresolvable = fail closed. - Author preflight rejects commit -C/-c/--amend that would leave a non-agent author; rebase/cherry-pick of upstream history pass. - Signing-disable via -c/--no-gpg-sign rejected at argv. - Process-level mutation tests spawn the real multicall and go red when each enforcement layer is removed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: Polish mobile timeline navigation (#5874) chore(release): release Buzz Desktop version 0.5.17 (#6234) fix(prompt): simplify pickup follow-through (#6186) fix(mcp): scope todo usage (#6216) fix(desktop): bound remote agent mention authorization (#6224) fix: bump h2 for RUSTSEC-2026-0258 (#6222) fix(desktop): bind presence retry timers (#6213) ci: make file-size policy a first-class gate (#6187) fix(desktop): eliminate mounted-view CPU burn — compositor-safe shimmer, observer append fast path, poll-tick disk reads (#6198) chore(release): release Buzz Desktop version 0.5.16 (#6191) fix(desktop): restore release agent mentions (#6182) test(desktop): cover exact workflow batch limit (#6168) chore(release): release Buzz Desktop version 0.5.15 (#6173) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: fix(desktop-chrome): preserve balanced layout when sidebar collapses (#6000) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…c-agent-commit-identity * origin/main: feat(managed-agents): close five Claude Code agent-config gaps (#4557) chore(hooks): keep mobile analysis out of pre-commit (#6236) fix(shared-ui): delay hover disclosures by default (#5821) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Address the reconciled Thufir/Gurney round-2 findings on the deterministic agent git-identity gate: - Injected identity args now outrank caller `-c`/GIT_CONFIG_PARAMETERS/ include.path/repo config, and a tampered manifest fails closed (C1). - Shell (`!`) push aliases are treated as opaque and rejected before any probe, so an alias cannot transmit before verification (C2). - Author-preserving commit modes gain a patch-id exemption so legitimate rebases of upstream human commits pass while new human-authored commits are still refused (I3). - BUZZ_PRIVATE_KEY outranks NOSTR_PRIVATE_KEY at both layers and the canonical key is restaged unconditionally as the child NOSTR_PRIVATE_KEY, so the harness and dev-mcp shim can never install split identities (I5). - The push predictor's subprocess probe is bounded by a hard timeout and fails closed on expiry (I6). Adds a real-binary integration test driving the buzz-acp spawn path so the install_git_identity wiring is regression-covered, plus mutation-sensitive unit tests for the key precedence and timeout paths. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
An ordinary (non-shell) git alias whose body carries global `-c`/`--config-env` config is expanded by git in-process after the wrapper's injected identity/signing `-c` options, so the alias-added config outranks the authority and could silently re-author or unsign a commit. enforce() inspects only the literal argv and never the alias body, so it could not catch this — a repo-local alias recreated the original human-attribution leak through the managed wrapper. Add a pre-exec verify_alias_safety pass that resolves the effective alias chain and refuses any alias whose expansion introduces global configuration, per the favor-rejection principle rather than modeling git's full alias grammar. Plain-subcommand aliases keep working. Shell (`!`) aliases stay out of scope here: their git invocations re-enter the wrapper on PATH and push-bearing ones are already rejected as opaque. Also gate the unconditional `nostr::ToBech32` import in git_identity_enforcement.rs behind #[cfg(unix)] (its only use is a unix-only test) so the Windows clippy gate stops failing on unused-import under -D warnings. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…shell aliases The prior blocklist scanned an alias body for known-bad config tokens, but git's quote-aware alias parser dequotes `'-c' 'user.email=…'` into a live config channel that a naive whitespace scan never sees — a parser-parity bypass. And `!` shell aliases were treated as safe on the commit path on the false premise that their inner git re-enters this wrapper; git prepends its own exec-path to PATH, so the inner git is the real binary and its -c outranks the inherited env authority, committing as an arbitrary human, unsigned. Invert verify_alias_safety to an allowlist: a non-shell alias is admitted only when every body token is a trivially-safe bare word (no quote/backslash, no -c/--config-env in any spelling, no =-valued option); anything else is refused without modeling git's grammar. Reject all shell aliases outright in a managed session, commit path included. This makes the whole config-injection class end by construction. Gurney's certified shapes (ci=commit, st=status, lg=log --oneline, pub=push origin main) stay allowed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: Revert "fix(acp): gate relay-signed workflow messages on their attributed author" (#6311) fix(desktop): morph the drawer panel icon instead of sliding it (#6306) feat(desktop): refine repository-aware project workspaces (#6003) Fix mobile Activity thread navigation (#5850) perf(desktop): parallelize relay agent directory rebuild (#6258) Refine the mobile emoji picker (#5853) fix(desktop): exclude archived agents from nest, order regeneration (#5905) Add font size and conversation density preferences (#5644) fix(desktop): emit camelCase config-write payload fields (#6062) fix(desktop): downscale large avatars for agent-share PNG body (#6260) fix(desktop): preserve early relay auth challenges (#3320) Polish mobile message actions (#5873) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
… policy A bare-word alias whose body carried identity/signing flags (`commit --author … --no-gpg-sign`) passed the allowlist because every token is a plain bare word, and enforce()/verify_commit_author() keyed on the literal typed subcommand (the alias name, never the expanded `commit`) — so the flag preflights never fired. A repo-local alias through the managed wrapper could author as a human and disable signing. verify_alias_safety now returns the alias's fully-resolved expansion (typed globals + recursively-expanded command with accumulated body tokens and the user's trailing argv). run() holds that expansion to the same enforce() and verify_commit_author() preflight as a directly-typed command, keyed on the expanded subcommand. An alias can no longer do more than its expansion could typed directly, so there is no alias-specific flag list to maintain. Shell-alias and unclassifiable-syntax refusals are unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(buzz-acp): loosen workspace-scan guardrail to allow named paths (#6261) fix(buzz-dev-mcp): expand leading ~ in read_file/str_replace paths (#6271) perf(desktop): move five hot renderer paths from JS into Rust (#6024) fix(media): accept portrait video resolutions (#6058) fix(desktop): hide archived channels from #/Tab autocomplete (#6156) Unify mobile channel details (#6113) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Git continues resolving aliases after the wrapper reaches its bounded expansion limit. Refuse when the next command word remains an alias so a partial expansion cannot bypass managed identity and signing policy. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tity The deterministic-agent-commit-identity enforcement is unconditional: an agent always commits under its nostr identity. Sovereignty (VISION_SOVEREIGN.md) says the operator, not the platform, decides whose identity their agent's commits carry on their own machine. Add BUZZ_GIT_IDENTITY=agent|user (default agent, settable per-agent via persona env). `user` reuses the existing review-hardened unconfigured-session path — no git wrapper on PATH, no manifest, no injected authorship/signing config — so vanilla git resolves the operator's own identity. The nostr credential helper (relay git-over-HTTP auth) keeps installing in user mode: auth is not attribution. The mode is read once at spawn by the harness and shim, never by the wrapper per-invocation, so an agent cannot disable enforcement mid-session. An unrecognized value fails the spawn loudly rather than silently choosing a mode. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: (40 commits) chore(release): release Buzz Desktop version 0.5.18 (#6489) fix(desktop): simplify duplicate agent provenance (#6401) test(benchmarks): expand Buzz-native dataset (#6448) fix(desktop): sender names in notifications + macOS click-through routing (#6427) docs: clarify two-layer moderation ownership (#6481) Fix mobile thread tail and iOS channel header (#6399) chore(deps): pin earshot below 1.2.0 pending a VAD threshold re-pick (#6392) polish(desktop): finish Projects navigation and context chrome (#6429) fix(desktop): clarify add agents channel action (#6374) Repair stale large channel roster snapshots (#6251) feat(desktop-messages): show compact Buzz link metadata (#6252) feat(workflows): reply in-thread from send_message action (#6178) perf(desktop): split discover_acp_providers into cheap and forced paths (#6330) fix(desktop): restore recent channel sorting (#6402) fix(desktop): isolate main timeline stacking context from focus drawer (#6398) fix(desktop): make reconnect repair lossless (#6415) fix(hooks): scope pre-push lanes to branch merge-base diff (#6423) Enforce a three-day dependency cooldown (#6426) perf(desktop): resolve references without directory scans (#6328) feat(llm): stamp thinking effort on call-completed log line (#6424) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
…lper In BUZZ_GIT_IDENTITY=user mode the harness returned Ok(None) before resolving the agent key, so a headless BUZZ_PRIVATE_KEY-only launch left the dev-mcp shim's credential-helper-only branch with no key — the toggle silently removed relay git AUTH, not just attribution. auth != attribution must hold on every launch path, and an operator who set user mode on a configured session has not unconfigured their auth. Resolve the canonical key (same BUZZ_PRIVATE_KEY-over-NOSTR_PRIVATE_KEY, child-over-process precedence) and stage it as the child's NOSTR_PRIVATE_KEY BEFORE the user-mode early return. user mode still installs no wrapper, manifest, keyfile, or injected config on the harness side; no key at all is still Ok(None). Wrapper unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The extra_env merge in AcpClient::spawn only staged a persona value when the parent process env lacked that key, so a global BUZZ_GIT_IDENTITY silently defeated every per-agent override in both directions — the child-over-process lookup in install_git_identity never saw the dropped value. Treat the var as an operator-controlled exception and always stage the persona value; the general parent-wins semantics for other keys are unchanged. Also derive Debug on Shim so the shim seam test's expect_err compiles under clippy -D warnings (--workspace --all-targets). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…c-agent-commit-identity * origin/main: fix(desktop): restore human barge-in over agent TTS in huddles (#6431) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The test mutated process-global env across .await points and unconditionally removed the variable, clobbering any caller-supplied value instead of restoring it. Add an RAII guard that captures the prior value and restores-or-removes on drop (including on assertion panic). It is the only env-mutating spawn test in buzz-acp, so no cross-test serialization is required. Both-direction real-merge-loop assertions are unchanged. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
I found three blocking contract failures at c64e62e599ffaaf223a111c34ea600d341967f57:
-
P1: managed git can create and push unsigned agent-authored commits.
enforce()rejects--no-gpg-signforcommit|am|tag|rebase|cherry-pick|revert, but notmerge/pull; the outgoing gate then checks only author email and never verifies signature presence or the expected key. A focused real-wrapper reproduction created an unsigned merge withgit merge --no-gpg-sign --no-ff, observed%G? = N, and pushed it successfully.git commit-treeproduces the same systemic bypass through the installed wrapper. This contradicts the PR’s “every commit is automatically signed” contract. At minimum cover every signing-capable porcelain path; the robust boundary is to require a valid expected-key NIP-GS signature on each new agent-authored outgoing commit, with merge/plumbing push regressions. -
P1:
BUZZ_GIT_IDENTITY=usercan erase relay git authentication through descriptor env. Desktop stages its credential-helperGIT_CONFIG_*entries and then writesdescriptor.envafterward (runtime.rs:781-811). A descriptor withBUZZ_GIT_IDENTITY=userplusGIT_CONFIG_COUNT=0reaches the harness; user mode intentionally returns before installing identity config, leaving the clobbered count in place. That violates the explicit auth-is-not-attribution guarantee. Preserve/reapply the helper after descriptor env or reserve the completeGIT_CONFIG_*family, and test the actual Desktop-to-harness layering rather than a freshCommand. -
P2: the changed Nest guidance is not delivered to existing installs and conflicts with #6707. This PR rewrites
nest_agents.mdbut leavesNEST_AGENTS_VERSIONat 4 and adds no content/upgrade test, so existing v4 Nests never receive it. #6707 bumps 4→5, tests refresh/preservation, and intentionally removes the unconditional humanSigned-off-bymandate that #6177 retains. The two branches edit the same block and #6707’s content test rejects #6177’s exact wording. Reconcile onto one policy contract; keep #6707’s version bump and upgrade coverage rather than landing both as-is.
The runtime mechanism in #6177 is broader than #6707: #6707 is policy-neutral generated guidance and does not replace identity enforcement. Its wording already allows a managed agent identity while keeping repository policy authoritative, so it is the safer documentation base to combine with a corrected runtime implementation.
Current CI for #6177 is green and git diff --check passes, but neither covers these boundary failures.
Problem
Agent sessions could commit under the human operator's ambient git identity. Only
buzz-dev-mcp's shim applied the nostr author/signingGIT_CONFIG_*env, and only to its own shell-tool children — the native shells of claude-code, codex, and goose never saw it. A baregit commitin those shells resolved to whateveruser.name/user.emailthe repo or global config carried, erasing the AI-attribution signal (e.g. #3140, which landed under a human identity).Change
Makes the agent commit author identity machine-managed and deterministic across every harness. Trailers still credit the human operator (
Co-authored-by+Signed-off-by); that policy is unchanged.buzz-git-identity(new crate) — the single source of truth for the author/email, NIP-GS signing, and keyfile logic, consumed by both the dev-mcp shim and the harness so an agent commits under a byte-identical identity regardless of which surface applied it. The identity email is<64-hex-pubkey>@<relay-host>; the author name is the sanitized Buzz display name (falls back to thenpub).0600identity manifest. The enforcement wrapper locates its own install directory by canonicalizing thegitonPATHagainst its own executable — the same trust channel it uses to find the realgit, immune to environment rewriting — reads the expected identity from the manifest, and injects it directly as the highest-precedence config, never trustinggit config user.email. A manifest that is missing or fails to parse in an otherwise-managed session fails the invocation closed rather than silently dropping enforcement. Manifest present = enforce; genuinely absent (keyless/unconfigured session) = passthrough.gitenforcement wrapper (git_wrapper.rs) — installed onPATHahead of the real binary.-cargs at the front of the resolved command, so it outranks caller-supplied-c,GIT_CONFIG_PARAMETERS,include.path, and repo/global config alike. ScrubsGIT_AUTHOR_*/GIT_COMMITTER_*; rejects-c user.*,--config-env=user.*,--author, and--reset-author(scoped tocommit/amsogit log --authorstill works).commit -C/-c <sha>and--amendcreate new commits that reuse another commit's author; injected config cannot override a reused author, so the wrapper inspects the resulting author and rejects any commit-creating mode that would leave a non-agent author. Ordinary commits and amends of the agent's own commits pass.git push --dry-run --porcelain --no-verify <original args>to obtain the exact resolved update set — covering config-defined and inline-c alias.*git aliases,-C/--git-dir,--all/--mirror/--tags, configremote.*.push, and wildcard refspecs — then refuses any outgoing commit not authored by the agent identity, naming the offending sha and email. A human-authored commit is exempted only when it is a patch-id-identical replay of a commit already upstream (a legitimately cherry-picked/rebased human commit — correct attribution, not new agent work masquerading as someone else); any other non-agent author is refused. The dry-run probe is bounded by a hard timeout; a timeout, an unresolvable update set, or any dry-run failure fails closed. The--no-verifyon the internal dry-run keeps it from double-running the repo's own pre-push hooks; the real push keeps its hooks, and enforcement runs unconditionally so--no-verifyon the real push cannot bypass it.-coptions, so an alias could otherwise plant higher-precedence config that re-authors or unsigns the commit — and git's quote-aware alias parser sees tokens differently from a naive whitespace scan ('-c' 'user.email=…'dequotes to real config). Rather than model that grammar, the wrapper admits a non-shell alias only when every token of its resolved body is a trivially-safe bare word: no quote or backslash characters, no-c/--config-envchannel, and no=-valued option. Anything else is refused. Shell (!) aliases are refused outright in a managed session — git runs their body with the real git ahead of the wrapper onPATH, so an inner-coutranks the inherited authority and can commit or push under an arbitrary identity, unsigned; there is no safe subset to allow. A bare-word alias can still carry identity/signing flags (commit --author … --no-gpg-sign), so on success the alias is expanded and its resolved command — accumulated body tokens across up to ten alias substitutions plus the caller's trailing argv; if another alias remains at that bound, the wrapper refuses rather than treating a partial expansion as resolved — is held to the identical identity/signing policy as the same command typed directly (enforce()and the commit-author preflight, keyed on the expanded subcommand). An alias can therefore never do more than its expansion could typed directly, and there is no alias-specific flag list to keep in sync. Bare-word aliases whose expansion is clean keep working (alias.ci = commit,alias.st = status,alias.lg = log --oneline,alias.pub = push origin main).-c commit.gpgSign=falseand--no-gpg-signare rejected at argv; env-based signing-disable is defeated by the injected highest-precedence config.buzz-acp) —AcpClient::spawnwrites the keyfile and manifest, installs the wrapper plus the nostr signer/credential helpers viabuzz-acp's own multicall personalities, prepends the wrapper dir to the childPATH, and applies the identity + signingGIT_CONFIG_*composed over the desktop's per-URL credential helper. The key is sourcedBUZZ_PRIVATE_KEY(the documented required secret) beforeNOSTR_PRIVATE_KEYat both the command and process-env layers, and the canonical key is restaged unconditionally as the child'sNOSTR_PRIVATE_KEYso the harness and dev-mcp shim can never install split identities. A managed session fails closed when deterministic identity cannot be installed; sessions with no nostr key are skipped entirely, so test spawns and unconfigured sessions are unchanged. Unix-only.base_prompt.md,nest_agents.md) — identity is machine-managed; credit the operator viaCo-authored-by/Signed-off-bytrailers, neveruser.name/user.email/-c/--author.BUZZ_GIT_IDENTITY=agent|user, defaultagent, settable per-agent via persona env).agentis everything above.userreuses the existing review-hardened unconfigured-session path — the harness installs no wrapper onPATH, no manifest, no keyfile, and no injected authorship/signing config, so vanilla git resolves the operator's own repo/global identity and signing. This matchesVISION_SOVEREIGN.md: the operator, not the platform, decides whose identity their agent's commits carry on their own machine. The nostr credential helper (relay git-over-HTTP auth) keeps installing inusermode, and the harness still stages the canonical key as the child'sNOSTR_PRIVATE_KEYbefore itsuser-mode early return so the helper can authenticate on every launch path (headless as well as desktop) — auth is not attribution, and settinguseron a configured session must not silently drop relay git auth. The mode is read once at spawn by the harness and dev-mcp shim, never by the wrapper per-invocation, so an agent cannotexport BUZZ_GIT_IDENTITY=usermid-session to disable enforcement. An unrecognized value fails the spawn loudly, naming the variable and its two values, rather than silently choosing a mode.usermode drops the operator out of commit-level AI attribution — deliberate, their claim to make; in that mode theCo-authored-by/Signed-off-bytrailers are redundant since the commit already is the operator's identity.Scope and ceiling
This is a deterministic best-effort local control, not an adversarial sandbox. Enforcement is a
PATHwrapper sharing the OS user with the agent, so it closes accidental identity leakage — the entire class behind #3140 — but does not stop a deliberate bypass: invoking/usr/bin/gitby absolute path,env -i, replacing the wrapper onPATH, deleting the manifest, or committing via libgit2/jj all sidestep it by design. A hard guarantee that holds regardless of what the agent runs would require a receive-side gate on the git host; that remains a possible separate follow-up. This PR intentionally targets the default-path commit/push surface, which is where the missed-attribution signal originates.Verification
Beyond unit tests (
buzz-git-identity66,buzz-acp817 unit + integration suites), the wrapper mechanism is exercised end-to-end by process-level tests (git_identity_enforcement.rs) that spawn the realbuzz-acp-as-gitmulticall against a real repo with a manifest present:-c user.email=…,--author=…) is rejected;<display-name> <hex@relay>;alias.quoted = '-c' 'user.email=…' commit) and a shell (!) commit alias are each refused before git runs and leaveHEADunchanged, while a plain-subcommand alias still resolves and commits agent-authored; a bare-word alias carrying identity/signing flags (alias.human = commit --author … --no-gpg-sign,--no-gpg-signalone, and a two-hop chain) is expanded and refused by the same policy as the typed command,HEADunchanged; a chain of exactly ten aliases reachingcommitremains usable, while an eleventh alias is refused before git runs with unbornHEADand zero commit objects;Each process-level test is mutation-verified: nulling the harness's
install_git_identitywiring, or dropping theenforce/verify_pushdispatch, turns the corresponding test red — confirming the layer is wired into the process boundary, not merely unit-covered. The key-precedence and bounded-probe paths carry their own mutation-sensitive unit tests. The keyfile-lifecycle test (keyfile_lifecycle.rs) spawns the real binary on its error-exit path and confirms the0600keyfile is deleted on every exit.The
BUZZ_GIT_IDENTITYtoggle is covered at three layers:GitIdentityModeparsing unit tests (unset →agent, exactagent/userwith whitespace tolerance, and loud rejection of typos/case-variants/empty naming the variable and both values); harness gate tests (userinstalls no identity dir orGIT_CONFIG_*, a persona-staged value outranks process env, explicitagentstill writes the manifest, an invalid value errors at spawn); and shim seam tests (agentinstalls thegitwrapper + signer + manifest + authorship config;userkeeps only thegit-credential-nostrhelper and itsnostr.keyfilepointer with no wrapper, signer, manifest, or authorship/signing config; unset defaults toagent; invalid fails install).Notes
Co-authored-bytrailer email.git-sign-nostr.